// Copyright (C) 2009 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using MmmDoc.Database;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace MmmDoc
{
	/// <summary>Parses the XML files generated by the -doc compiler switch.</summary>
	internal sealed class ParseXml
	{
		// TODO: It would be simpler to rewrite this using XPathDocument but when I did
		// that I started getting intermittent errors like so:
		// ERROR:mini-trampolines.c:341:mono_magic_trampoline: assertion failed: (*vtable_slot)
		// They always happened inside the DateTime class ctor and went away when the program
		// was perturbed by adding things like CWLs.
		public void Parse(string file)
		{
			XmlDocument doc = DoReadXml(file);
			DoParseRoot(doc);
		}
		
		public void ResolveHelpDefinitions(Document doc)
		{
			CodeReference.Resolve(doc);
			
			foreach (KeyValuePair<CodeReference, Definition> entry in m_members)
			{
				if (entry.Key.Element != null)
					entry.Key.Element.Definition = entry.Value;
			}
		}
		
		#region Private Methods
		private XmlDocument DoReadXml(string path)
		{
			XmlDocument xml;
			
			using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
			{
				using (XmlReader reader = XmlReader.Create(stream))
				{
					xml = new XmlDocument();
					xml.Load(reader);
				}
			}
			
			return xml;
		}
		
		private void DoParseRoot(XmlDocument xml)
		{
			foreach (XmlNode child in xml.ChildNodes)
			{
				if (child is XmlElement)
				{
					if (child.Name == "doc")
					{
						DoParseDoc(child);
					}
					else
						Console.Error.WriteLine("Unknown root element: {0}", child.Name);
				}
			}
		}
		
		private void DoParseDoc(XmlNode doc)
		{
			foreach (XmlNode child in doc.ChildNodes)
			{
				if (child is XmlElement)
				{
					switch (child.Name)
					{
						case "assembly":
							break;
							
						case "members":
							DoParseMembers(child);
							break;
							
						default:
							Console.Error.WriteLine("Unknown doc element: {0}", child.Name);
							break;
					}
				}
			}
		}
		
		private void DoParseMembers(XmlNode members)
		{
			foreach (XmlNode child in members.ChildNodes)
			{
				if (child is XmlElement)
				{
					if (child.Name == "member")
						DoParseMember(child);
					else
						Console.Error.WriteLine("Unknown members element: {0}", child.Name);
				}
			}
		}
		
		private void DoParseMember(XmlNode member)
		{
			if (member.Attributes["name"] != null)
			{
				string name = member.Attributes["name"].Value;
				var definition = new Definition();
				
				foreach (XmlNode child in member.ChildNodes)
				{
					if (child is XmlElement)
					{
						switch (child.Name)
						{
							case "example":
								definition.Examples.Add(DoParseMarkup(child, name));
								break;
							
							case "exclude":
								definition.Excluded = true;
								break;
								
							case "exclude-if-empty":
								definition.ExcludeIfEmpty = true;
								break;
								
							case "exception":
								DoParseNamedMarkup(child, definition.Exceptions, "cref", name);
								break;
								
							case "param":
								DoParseNamedMarkup(child, definition.Params, "name", name);
								break;
								
							case "permission":
								DoParseNamedMarkup(child, definition.Permissions, "name", name);
								break;
								
							case "remarks":
								if (definition.Remarks != null)
									Console.Error.WriteLine("overwriting existing remarks (in {0})", name);
								definition.Remarks = DoParseMarkup(child, name);
								break;
								
							case "returns":
								if (definition.Returns != null)
									Console.Error.WriteLine("overwriting existing returns (in {0})", name);
								definition.Returns = DoParseMarkup(child, name);
								break;
								
							case "seealso":
								if (child.Attributes["cref"] != null)
									definition.SeeAlso.Add(new CodeReference(child.Attributes["cref"].Value));
								else
									Console.Error.WriteLine("seealso element is missing cref attribute (in {0})", name);
								break;
								
							case "summary":
								if (definition.Summary != null)
									Console.Error.WriteLine("overwriting existing summary (in {0})", name);
								definition.Summary = DoParseMarkup(child, name);
								break;
								
							case "typeparam":
								DoParseNamedMarkup(child, definition.Params, "name", name);
								break;
								
							case "value":
								if (definition.Value != null)
									Console.Error.WriteLine("overwriting existing value (in {0})", name);
								definition.Value = DoParseMarkup(child, name);
								break;
								
							default:
								Console.Error.WriteLine("Unknown member element: {0}", child.Name);
								break;
						}
					}
				}
				
				m_members.Add(new CodeReference(name), definition);
			}
			else
				Console.Error.WriteLine("Member is missing a name attribute");
		}
		
		private void DoParseNamedMarkup(XmlNode member, List<LabeledMarkup<string>> help, string attrName, string context)
		{
			if (member.Attributes[attrName] != null)
			{
				string name = member.Attributes[attrName].Value;
				var markup = DoParseMarkup(member, context);
				Contract.Assert(!name.Contains(":"), name + " has a colon");
				
				help.Add(new LabeledMarkup<string>(name, markup));
			}
			else
				Console.Error.WriteLine("Member is missing a {0} attribute", attrName);
		}
		
		private void DoParseNamedMarkup(XmlNode member, List<LabeledMarkup<CodeReference>> help, string attrName, string context)
		{
			if (member.Attributes[attrName] != null)
			{
				string name = member.Attributes[attrName].Value;
				var markup = DoParseMarkup(member, context);
				
				help.Add(new LabeledMarkup<CodeReference>(new CodeReference(name), markup));
			}
			else
				Console.Error.WriteLine("Member is missing a {0} attribute", attrName);
		}
		
		private SequencedMarkup DoParseMarkup(XmlNode parent, string context)
		{
			var result = new List<Markup>();
			
			foreach (XmlNode child in parent.ChildNodes)
			{
				if (child is XmlText)
				{
					DoSpellCheck(context, child.InnerText);
					result.Add(new TextMarkup(child.InnerText));
				}
				else if (child is XmlElement)
				{
					switch (child.Name)
					{
						case "a":
							if (child.Attributes["href"] != null)
								result.Add(new LinkMarkup(child.Attributes["href"].Value, child.InnerText));
							else
								Console.Error.WriteLine("a element is missing href attribute (in {0})", context);
							break;
							
						case "c":
							result.Add(new CodePhraseMarkup(child.InnerText));
							break;
							
						case "code":
							SequencedMarkup code = DoParseMarkup(child, context);
							result.Add(new CodeBlockMarkup(code.Markup));
							break;
							
						case "list":
							string type;
							if (child.Attributes["type"] != null)
								type = child.Attributes["type"].Value;
							else
								type = "bullet";
							
							if (type == "bullet")
							{
								Markup[] items = DoParseListItems((XmlElement) child, context);
								result.Add(new BulletListMarkup(items));
							}
							else if (type == "number")
							{
								Markup[] items = DoParseListItems((XmlElement) child, context);
								result.Add(new NumberListMarkup(items));
							}
							else
								Console.Error.WriteLine("{0} lists are not implemented (in {1})", type, context);
							break;
							
						case "para":
							SequencedMarkup para = DoParseMarkup(child, context);
							result.Add(new ParagraphMarkup(para.Markup));
							break;
							
						case "paramref":
						case "typeparamref":
							if (child.Attributes["name"] != null)
								result.Add(new NameReferenceMarkup(child.Attributes["name"].Value));
							else
								Console.Error.WriteLine("{0} element is missing name attribute (in {1})", child.Name, context);
							break;
							
						case "see":
							if (child.Attributes["cref"] != null)
								result.Add(new SeeMarkup(child.Attributes["cref"].Value, child.InnerText));
							else
								Console.Error.WriteLine("see element is missing cref attribute (in {0})", context);
							break;
							
						default:
							Console.Error.WriteLine("Unknown markup element: {0}", child.Name);
							break;
					}
				}
			}
			
			return new SequencedMarkup(result.ToArray());
		}
		
		private Markup[] DoParseListItems(XmlElement list, string context)
		{
			var result = new List<Markup>();
			
			foreach (XmlNode child in list.ChildNodes)
			{
				if (child is XmlElement)
				{
					switch (child.Name)
					{
						case "item":
							Markup desc = DoParseListDesc((XmlElement) child, context);
							if (desc != null)
								result.Add(desc);
							break;
							
						default:
							Console.Error.WriteLine("{0} list elements are not implemented (in {1})", child.Name, context);
							break;
					}
				}
			}
			
			return result.ToArray();
		}
		
		private Markup DoParseListDesc(XmlElement item, string context)
		{
			Markup result = null;
			
			foreach (XmlNode child in item.ChildNodes)
			{
				if (child is XmlElement)
				{
					switch (child.Name)
					{
						case "description":
							result = DoParseMarkup(child, context);
							break;
							
						default:
							Console.Error.WriteLine("{0} item elements are not implemented (in {1})", child.Name, context);
							break;
					}
				}
			}
			
			return result;
		}
		
		private void DoSpellCheck(string context, string text)
		{
			if (Hunspell.Instance != null)
			{
				if (context.Length > 2 && context[1] == ':')	// strip off the T: or M:
					context = context.Substring(2);
					
				string[] misspelled = Hunspell.Instance.Check(text);
				foreach (string word in misspelled)
				{
					string[] suggestions = Hunspell.Instance.Suggest(word);
					Console.Error.WriteLine("{0} is misspelled in {1}", word, context);
					if (suggestions.Length > 0)
						Console.Error.WriteLine("    {0}", string.Join(", ", suggestions));
				}
			}
		}
		#endregion
		
		#region Fields
		private Dictionary<CodeReference, Definition> m_members = new Dictionary<CodeReference, Definition>();
		#endregion
	}
}
